package dslab.core;

import dslab.util.Message;

import java.io.*;
import java.net.Socket;
import java.util.List;

/**
 * abstract handler class that acts as a shared codebase for DMTP handling;
 * it is expected that implementers provide a handle(To|From|Send) implementation
 */
public abstract class DMTPHandler implements Runnable, DMTPProtocol {

    // 30 seconds timeout constant
    private static final int TIMEOUT = 30*1000;
    protected static final String ILLEGAL_STATE_WARNING = String.format("error illegal protocol state%n");

    protected final String componentId;
    protected final Socket socket;
    protected final Message message;

    protected SessionState state;

    /**
     * enum representing possible states of this particular DMTP protocol implementation
     */
    protected enum SessionState {
        NoMessages, EditingMessage, Done
    }

    /**
     * creates a new (abstract) DMTPHandler, meant to be overriden and extended
     * @param componentId string representation of component id
     * @param socket client connection socket to handle, opened elsewhere
     * @throws IOException exception in case socket operation fails
     */
    public DMTPHandler(String componentId, Socket socket) throws IOException {
        this.componentId = componentId;
        this.socket = socket;
        this.message = new Message();
        this.state = SessionState.NoMessages;
        // register timeout
        this.socket.setSoTimeout(TIMEOUT);
    }

    @Override
    public void run() {
        System.out.printf("@%s: begin run%n", componentId);
        // read and service request on socket
        try (
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
        ) {
            // announce spoken protocols
            initMessage(out);

            // loop over requests (individual lines)
            String input;
            while ((input = in.readLine()) != null) {
                System.out.printf("@%s: processing input %s%n", componentId, input);
                // handle line
                processRequest(out, input);

                // break on quit after handling quit
                if (state == SessionState.Done)
                    break;
            }

            // finally close connection
            if (!socket.isClosed())
                socket.close();
        } catch (IOException | InterruptedException exception) {
            System.err.printf("@%s: a fatal error has occurred: the connection was interrupted%n", componentId);
            exception.printStackTrace();
        }
        System.out.printf("@%s: end run%n", componentId);
    }

    @Override
    public abstract void handleTo(Writer out, List<String> addresses, String addressesString) throws IOException;

    @Override
    public abstract void handleFrom(Writer out, String address) throws IOException;

    @Override
    public abstract void handleSend(Writer out) throws IOException, InterruptedException;

    @Override
    public void initMessage(Writer out) throws IOException {
        // communicate result
        out.write(String.format("ok DMTP2.0%n"));
        out.flush();
    }

    @Override
    public void handleBegin(Writer out) throws IOException {
        if (isNotInState(out, SessionState.NoMessages, ILLEGAL_STATE_WARNING))
            return;

        // communicate result
        out.write(String.format("ok%n"));
        out.flush();
        //message.reset();
        state = SessionState.EditingMessage;
    }

    @Override
    public void handleSubject(Writer out, String subject) throws IOException {
        if (isNotInState(out, SessionState.EditingMessage, ILLEGAL_STATE_WARNING))
            return;

        // communicate result
        out.write(String.format("ok%n"));
        out.flush();
        message.subject = subject;
    }

    @Override
    public void handleData(Writer out, String data) throws IOException {
        if (isNotInState(out, SessionState.EditingMessage, ILLEGAL_STATE_WARNING))
            return;

        // communicate result
        out.write(String.format("ok%n"));
        out.flush();
        message.data = data;
    }

    @Override
    public void handleHash(Writer out, String hash) throws IOException {
        if (isNotInState(out, SessionState.EditingMessage, ILLEGAL_STATE_WARNING))
            return;

        // communicate result
        out.write(String.format("ok%n"));
        out.flush();
        message.hash = hash;
    }

    @Override
    public void handleQuit(Writer out) throws IOException {
        // communicate result
        out.write(String.format("ok bye%n"));
        out.flush();
        state = SessionState.Done;
    }

    @Override
    public void raiseWarning(Writer out, String warning) throws IOException {
        // communicate error
        out.write(warning);
        out.flush();
    }

    @Override
    public void raiseError(Writer out, String error) throws IOException {
        raiseWarning(out, error);
        state = SessionState.Done;
    }

    /**
     * verifies whether the session is in the correct state and possibly raises warning
     * @param out writer bound to client connection output stream
     * @param state SessionState the session needs to be in
     * @param errorMessage String content of warning to raise
     * @return true if session is not in specified state
     * @throws IOException thrown when a write operation fails
     */
    protected boolean isNotInState(Writer out, SessionState state, String errorMessage) throws IOException {
        boolean inState = this.state == state;
        if (!inState) raiseWarning(out, errorMessage);
        return !inState;
    }
}
